<?php
/**
 * Created by PhpStorm.
 * User: Appla
 * Date: 2017/5/16
 * Time: 10:43
 */

namespace util\http\monitor;

use artisan\cache;
use Redis;
use DateTime;
use SplQueue;

/**
 * 统计Curl请求的情况记录HTTP状态和Curl请求状态
 * HTTP status codes:
 * @see https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 * @see https://curl.haxx.se/libcurl/c/libcurl-errors.html
 * curl error code最大没有超过100,所以直接和HTTP状态码混用了
 * Class CurlRequestLogger
 * @package util\http\monitor
 */
class CurlRequestLogger
{
    const CACHE_KEY_TPL = 'CurlRequestLogs:url:%s:%s';

    const CACHE_KEY_PREFIX = 'CurlRequestLogs:url:';

    const CACHE_KEY_TIMEOUT_TAG = 'requestTimeoutOptionLog';

    const CACHE_KEY_REQUEST_TIME_TAG = 'requestTimeCostLog';

    const STR_SEPARATOR = '|';

    /**
     * 原始信息队列key
     */
    const REQUEST_INFO_QUEUE = 'CurlRequestLogs:originalInfo:queue';

    const CACHE_INSTANCE_ID = 'base2';

    const CACHE_INSTANCE_DB = 2;

    const TIME_FORMAT = 'YmdHi';

    const MAX_TTL = 3600;

    /**
     * 记录单次请求的数据
     */
    const LOG_PER_REQUEST_INFO = false;

    /**
     * 是否记录快宝内部请求, 暂时启用
     */
    const LOG_KB_INTERNAL_REQUEST = false;
    const KB_URI_SIGN = 'kuaidihelp.com';

    const SKIP_URI_PATTERN = '~^https?://(alipay\.com|weixin\.qq\.com|rds\.aliyuncs\.com)~';

    /**
     * 记录模式
     * 1.完全入队列
     * 2.解析后一次写入
     * 4.解析后马上写入
     */
    const MODE_TO_QUEUE = 1;
    const MODE_LAZY_WRITE = 2;
    const MODE_INSTANT_WRITE = 4;

    /**
     * 默认记录模式为批量写入
     * @var int
     */
    private $writeMode = self::MODE_LAZY_WRITE;

    /**
     * @var self
     */
    private static $instance;

    /**
     * @var \artisan\cache\PHPredisDriver|\Redis
     */
    private static $cacheInstance;

    /**
     * @var SplQueue
     */
    private static $queue;

    /**
     * @var bool
     */
    private static $logEnabled = true;

    /**
     * @var array
     */
    private $curlInfo;

    /**
     * @var int
     */
    private $instantiatedTime;

    /**
     * @var array
     */
    private $logData = [];

    /**
     * 记录每次请求的时间,用以计算访问时间分布
     * @var array
     */
    private $timeLogData = [];

    /**
     * CurlRequestLogger constructor.
     */
    protected function __construct()
    {
        $this->instantiatedTime = time();
        $this->writeMode !== self::MODE_INSTANT_WRITE && self::$queue = new SplQueue();
    }

    /**
     * @param resource $ch
     * @return array|mixed
     */
    public static function getCurlInfo($ch)
    {
        $info = [];
        if (is_resource($ch) && $curlInfo = curl_getinfo($ch)) {
            $curlInfo['error_info'] = curl_error($ch);
            $curlInfo['error_code'] = curl_errno($ch);
            $info = $curlInfo;
        }
        return $info;
    }

    /**
     * @param array|resource $info
     * @param array $requestInfo
     * @param string|null $response
     * @return bool|int
     */
    public static function log($info, array $requestInfo = [], $response = null)
    {
        if (self::$logEnabled) {
            if (is_resource($info)) {
                $info = self::getCurlInfo($info);
            }
            if (!empty($info['url']) && self::isAllowedUrl($info['url'])) {
                $requestInfo += [
                    'time_cost' => isset($info['total_time']) ? $info['total_time'] : 0,
                    'timeout'   => 0,
                ];
                $info += $requestInfo;
                $info['response'] = $response;
                return self::getInstance()->init($info)->write();
            }
        }
        return 0;
    }

    /**
     * @param string $url
     * @return bool
     */
    private static function isAllowedUrl($url) {
        return !empty($url) && (self::LOG_KB_INTERNAL_REQUEST || stripos($url, self::KB_URI_SIGN) === false);
    }

    /**
     * @param array $curlInfo
     * @return array
     * @TODO 调整为返回一个可以写入对象
     */
    private function prepareData(array $curlInfo)
    {
        $data = [];
        if ($curlInfo) {
            $cacheKey = $this->buildCacheKey();
            $data = &$this->logData[$cacheKey];
            $status_code = $curlInfo['http_code'] ?: $curlInfo['error_code'];
            $curlInfo['time_cost'] && ($this->timeLogData[$cacheKey][] = $curlInfo['time_cost']);
            $status_code_key = sprintf('status_code_%s', $status_code);
            //http状态码
            isset($data[$status_code_key]) && $data[$status_code_key]++ || $data[$status_code_key] = 1;
            //耗时
            isset($data['time_cost']) && ($data['time_cost'] += $curlInfo['time_cost']) || $data['time_cost'] = $curlInfo['time_cost'];
            $data['timeout'] = $curlInfo['timeout'];
            isset($data['total_hit']) && $data['total_hit']++ || $data['total_hit'] = 1;
            //正常返回, xml或者json
            $responseKeyType = self::isXmlOrJson($curlInfo['response']) ? 'valid_xml_or_json' : 'invalid_xml_or_json';
            isset($data[$responseKeyType]) && $data[$responseKeyType]++ || $data[$responseKeyType] = 1;
            $data['cache_key'] = $cacheKey;
        }
        return $data;
    }

    /**
     * @param string $str
     * @return bool
     */
    private static function isXmlOrJson($str)
    {
        return self::isValidJsonStr($str) || self::isValidXmlStr($str);
    }

    /**
     * @param string $str
     * @return bool
     */
    private static function isValidXmlStr($str)
    {
        return is_string($str) && stripos(trim($str), '<?xml') === 0;
    }

    /**
     * @param mixed $data
     * @return bool
     */
    private static function isValidJsonStr($data)
    {
        $return = false;
        if (is_string($data)) {
            json_decode($data);
            $return = json_last_error() === JSON_ERROR_NONE;
        } elseif (is_numeric($data)) {
            $return = true;
        }
        return $return;
    }

    /**
     * @return array|int
     */
    private function write()
    {
        $ret = 1;
        switch ($this->writeMode) {
            case self::MODE_TO_QUEUE:
                self::$queue->enqueue($this->curlInfo + ['cache_key' => $this->buildCacheKey()]);
                break;
            case self::MODE_INSTANT_WRITE:
                $ret = $this->instantWrite([$this->prepareData($this->curlInfo)]);
                break;
            case self::MODE_LAZY_WRITE:
            default:
                self::$queue->enqueue($this->prepareData($this->curlInfo));
        }
        return $ret;
    }

    /**
     * @param iterable $data
     * @return array
     */
    private function instantWrite($data)
    {
        $pipe = self::getCacheInstance()->pipeline();
        foreach ($data as $datum) {
            $mainKey = $datum['cache_key'];
            //访问时间分布需要单独记录, 超时单独记录
            self::LOG_PER_REQUEST_INFO && $pipe->append($mainKey . ':' . self::CACHE_KEY_REQUEST_TIME_TAG, self::STR_SEPARATOR . $datum['time_cost']);
            self::LOG_PER_REQUEST_INFO && $pipe->append($mainKey . ':' . self::CACHE_KEY_TIMEOUT_TAG, self::STR_SEPARATOR . $datum['timeout']);
            unset($datum['cache_key']); // $datum['timeout']
            foreach ($datum as $key => $item) {
                if($key === 'timeout') {
                    $pipe->hSet($mainKey, $key, $item);
                } else {
                    $pipe->hincrByFloat($mainKey, $key, (float)$item);
                }
            }
        }
        return $pipe->exec();
    }

    /**
     * @param iterable $data
     * @return array
     */
    private function pushToQueue($data)
    {
        $pipe = self::getCacheInstance()->pipeline();
        foreach ($data as $datum) {
            $pipe->lPush(self::REQUEST_INFO_QUEUE, json_encode($datum));
        }
        return $pipe->exec();
    }

    /**
     * @return \artisan\cache\PHPredisDriver|\Redis
     */
    private static function getCacheInstance()
    {
        if(self::$cacheInstance === null) {
            $cacheConfig = cache::getServer(self::CACHE_INSTANCE_ID);
            $r = new Redis();
            $r->connect($cacheConfig['host'], $cacheConfig['port'], $cacheConfig['timeout']);
            $r->select(self::CACHE_INSTANCE_DB);
            self::$cacheInstance = $r;
        }
        return self::$cacheInstance;
    }

    /**
     * @return string
     */
    private function buildCacheKey()
    {
        return sprintf(self::CACHE_KEY_TPL, $this->getUrl(), $this->getTimeStr());
    }

    /**
     * @return string
     */
    private function getUrl()
    {
        return self::parseUrl($this->curlInfo['url']);
    }

    /**
     * @param $url
     * @return string
     */
    public static function parseUrl($url)
    {
        return trim(preg_replace('~^https?://|\?.*$~si', '', $url), '/');
    }

    /**
     * @return string
     */
    private function getTimeStr()
    {
        return (new DateTime())->format(self::TIME_FORMAT);
    }

    /**
     * @param array $curlInfo
     * @return $this
     */
    private function init(array $curlInfo)
    {
        $this->curlInfo = $curlInfo;
        return $this;
    }

    /**
     * @return CurlRequestLogger
     */
    private static function getInstance()
    {
        return self::$instance ?: self::$instance = new self();
    }

    /**
     * 延迟写入
     */
    public function __destruct()
    {
        if (self::$logEnabled && self::$queue && !self::$queue->isEmpty()) {
            $this->writeMode === self::MODE_TO_QUEUE && $this->pushToQueue(self::$queue);
            $this->writeMode === self::MODE_LAZY_WRITE && $this->instantWrite(self::$queue);
        }
    }
}
